/*
 * Extract hidden media information from Sansa Fuze MP3 Player
 */

package jdefuze;
import java.io.*;
import java.util.*;

/**
 *
 * @author lehmann
 */
public class JDeFuze {

    static String version="0.2";

    // TODO: next 2 variables should move to command line options
    static String delim="|";
    static String vfatEncoding = "UTF8";

    static int songsCount=0;
    static int audiobooksCount=0;
    static int recordingsCount=0;
    static int photosCount=0;
    static int videosCount=0;
    static int podcastsCount=0;
    static int xCount=0, yCount=0;

    /* table to convert a nibble to a hex char, used by toHexString() */
    static char[] hexChar = {
        '0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    public static String toHexString(byte[] b) {
        StringBuffer sb = new StringBuffer(b.length * 2);
        for (int i = 0; i < b.length; i++) {
            // look up high nibble char
            sb.append(hexChar[(b[i] & 0xf0) >>> 4]);

            // look up low nibble char
            sb.append(hexChar[b[i] & 0x0f]);
        }
        return sb.toString();
    }

    public static void setSongsCount (int val) {
        songsCount=val;
    }

    public static void setAudiobooksCount (int val) {
        audiobooksCount=val;
    }

    public static void setRecordingsCount (int val) {
        recordingsCount=val;
    }

    public static void setPhotosCount (int val) {
        photosCount=val;
    }

    public static void setVideosCount (int val) {
        videosCount=val;
    }

    public static void setPodcastsCount (int val) {
        podcastsCount=val;
    }

    public static void setXCount (int val) {
        xCount=val;
    }

    public static void setYCount (int val) {
        yCount=val;
    }

    public static void readMtableHeader (DataInputStream inStream)
        throws IOException
    {
        setXCount          (Integer.reverseBytes(inStream.readInt()));
        setSongsCount      (Integer.reverseBytes(inStream.readInt()));
        setYCount          (Integer.reverseBytes(inStream.readInt()));
        setAudiobooksCount (Integer.reverseBytes(inStream.readInt()));
        setRecordingsCount (Integer.reverseBytes(inStream.readInt()));
        setPhotosCount     (Integer.reverseBytes(inStream.readInt()));
        setVideosCount     (Integer.reverseBytes(inStream.readInt()));
        setPodcastsCount   (Integer.reverseBytes(inStream.readInt()));
    }

    public static void showMtableHeader (Writer w)
        throws IOException
    {
        String s;
        String lineSep  = System.getProperty("line.separator");

        s = "# " + songsCount      + " Songs"
                + ", " + audiobooksCount + " Audiobooks"
                + ", " + recordingsCount + " Recordings"
                + ", " + photosCount     + " Photos"
                + ", " + videosCount     + " Videos"
                + ", " + podcastsCount   + " Podcasts"
                + lineSep;
        //s = s + "  others: " + xCount + " " + yCount + lineSep;
        w.write(s);
    }

    public static void skipNextStringFromMtab(DataInputStream inStream)
        throws IOException
    {
        char c, nullChar = '\u0000';
        do {
            c = inStream.readChar();
        } while (c != nullChar);
    }

    public static String getNextStringFromMtab(DataInputStream inStream)
        throws IOException
    {
        byte[] ba;
        int i, maxlen=128;
        char c, nullChar = '\u0000';
        boolean lastByteZero = false;
        Iterator<Character> it;
        ArrayList<Character> clist = new ArrayList<Character>();

        while ((c = inStream.readChar()) != nullChar) {
            clist.add(c);
        }

        // check if last byte is 0x00 //
        if ((clist.get(clist.size() - 1) & 0xFF) == 0) {
            lastByteZero=true;
            ba = new byte[clist.size() * 2 - 1];
        } else {
            ba = new byte[clist.size() * 2];
        }

        // copy into byte array
        i=0;
        it = clist.iterator();
        while(it.hasNext()) {
            c = it.next();
            ba[2*i]   = (byte) (c >> 8 & 0xFF);
            if ((i<(clist.size()-1)) || ! lastByteZero) ba[2*i+1] = (byte) (c & 0xFF);
            i++;
        }
        return(new String(ba, vfatEncoding));
    }

    public static String[] getMediainfoFromMtab (DataInputStream inStream)
        throws IOException
    {
        int i;
        String[] minfo = new String[10];
        byte[] header, storage;
        byte[] nullHeader = new byte[] {0,0,0,0,0,0,0,0,0,0,0,0};
        String s, fpath, fname;
        StringBuffer sb = new StringBuffer();
        sb.append(minfo[2]); //fpath
        sb.append(minfo[3]); //fname
        sb.append(delim);

        header = new byte[12];
        storage = new byte[6];
        inStream.read(header, 0, 12);
        if (Arrays.equals(header, nullHeader)) return null;
        minfo[0] = toHexString(header);
        inStream.read(storage, 0, 6);
        minfo[1] = new String(storage);
        minfo[2] = getNextStringFromMtab(inStream); // fpath
        minfo[3] = getNextStringFromMtab(inStream); // fname
        for (i=1; i<=6; i++) skipNextStringFromMtab(inStream);
        return(minfo);
    }

    public static int getRatingFromRes (DataInputStream inStream)
        throws IOException
    {
        int r;
        byte[] record;

        record = new byte[4];
        inStream.read(record, 0, 4);
        r = (int) record[1];
        return(r);
    }

    public static boolean isRecording (String[] minfo)
    {
        return (minfo[2].startsWith("\\RECORD\\"));
    }

    public static void showRatingInfo (DataInputStream mtabStream,
            DataInputStream resStream, String whichRatings, Writer w)
        throws IOException
    {
        int i, rating;
        String[] mediainfo;
        String lineSep = System.getProperty("line.separator");
        while ((mediainfo=getMediainfoFromMtab(mtabStream)) != null) {
            if (! isRecording(mediainfo)) {
                rating=getRatingFromRes(resStream);
                if ((whichRatings.equalsIgnoreCase("all")) || (rating > 0)) {
                    w.write(rating + delim
                            + mediainfo[1] + delim
                            + mediainfo[2] + delim
                            + mediainfo[3] + lineSep);
                }
            }
        }
    }

    public static void main(String[] args) {
        String fuzeDir  = null;
        String outFileName = null;
        DataInputStream mdataIn, rdataIn;
        Writer wOut;
        String fileSep = System.getProperty("file.separator");
        String ratings = System.getProperty("ratings", "");
        
        // Get the Fuze root directory from the argument line.
        if (args.length > 0) {
            fuzeDir = new String(args[0]);
        }
        if (args.length > 1) {
            outFileName = new String(args[1]);
        }
        if (fuzeDir == null) {
            System.out.println("usage: java -jar JDeFuze.jar fuzeDir <outFileName>");
            System.exit(-1);
        }

        try {
            mdataIn = new DataInputStream(new FileInputStream(
                    fuzeDir + fileSep + "MTABLE.SYS"));
            rdataIn = new DataInputStream(new FileInputStream(
                    fuzeDir + fileSep + "RES_INFO.SYS"));
            if (outFileName != null) {
                wOut = new OutputStreamWriter(new FileOutputStream(outFileName));
            } else {
                wOut = new OutputStreamWriter(System.out);
            }

            wOut.write("# JDeFuze version " + version
                + " (java version: " + System.getProperty("java.version")
                + " from "           + System.getProperty("java.vendor")
                + ")"                + System.getProperty("line.separator"));

            readMtableHeader(mdataIn);
            showMtableHeader(wOut);
            mdataIn.skipBytes(200);
            rdataIn.skip(1917);
            showRatingInfo(mdataIn, rdataIn, ratings, wOut);

            wOut.close();
            mdataIn.close();
            rdataIn.close();
        } catch (IOException e) {
            System.out.println("IO Exception =: " + e);
        }
    }
}
